package com.ezandroid.alphago.engine;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * Manages all game-play (i.e. non-visual) aspects of stone handling
 * Manages creation, updating, and removal of chains of like-colored stones
 */
public class MoveManager {

    /**
     * Holds all chains on the board
     */
    private Set<Chain> mChains;

    /**
     * Track filled coordinates and map them to the chain covering that location
     */
    private Map<Intersection, Chain> mFilled;

    /**
     * Track moves in order
     */
    private History<Move> mHistory;

    /**
     * Need to know board size to track liberties at edge of board
     */
    private int mBoardSize;

    /**
     * Track captured stones
     */
    // TODO: track captures and persist on save
    private int mWhitesCaptures;
    private int mBlacksCaptures;

    MoveManager(int size) {
        mChains = new HashSet<>();
        mFilled = new HashMap<>();
        mHistory = new History<>();
        mBoardSize = size;
        mWhitesCaptures = 0;
        mBlacksCaptures = 0;
    }

    MoveManager(int size, Set<Chain> savedChains) {
        mChains = new HashSet<>();
        for (Chain chain : savedChains) {
            Chain newChain = new Chain(chain.getStoneColor());
            newChain.getStones().addAll(chain.getStones());
            newChain.getLiberties().addAll(chain.getLiberties());
            mChains.add(newChain);
        }
        mHistory = new History<>();
        mBoardSize = size;
        mWhitesCaptures = 0;
        mBlacksCaptures = 0;

        mFilled = new HashMap<>();
        for (Chain chain : mChains) {
            updateFilled(chain, chain.getStones());
        }
    }

    public Set<Chain> getChains() {
        return mChains;
    }

    public Chain getChain(Intersection intersection) {
        return mFilled.get(intersection);
    }

    public History<Move> getHistory() {
        return mHistory;
    }

    /**
     * Returns true if the supplied coordinates are occupied by a stone
     */
    public boolean taken(Intersection intersection) {
        return mFilled.containsKey(intersection);
    }

    /**
     * Returns true if there is history after the present
     */
    public boolean hasFuture() {
        return mHistory.hasFuture();
    }

    /**
     * Returns true if there is history before the present
     */
    public boolean hasPast() {
        return mHistory.hasPast();
    }

    /**
     * Removes and returns last move from board
     */
    public Move undo() {
        Move move = mHistory.stepBack();
        if (move == null) {
            return null;
        }
        Stone stone = move.getStone();
        Set<Stone> captured = move.getCaptured();
        Chain chain = mFilled.get(stone.intersection);
        addToOpposingLiberties(stone);

        /* Remove and rebuild the current chain since it could have split, etc. */
        if (chain == null) {
            throw new IllegalStateException("Popped stone keyed to null chain");
        }
        mChains.remove(chain);
        Set<Stone> stones = chain.getStones();
        for (Stone s : stones) {
            mFilled.remove(s.intersection);
        }
        for (Stone s : stones) {
            if (s != stone) {
                incorporateIntoChains(s);
            }
        }
        /* Return captured stones to board */
        for (Stone s : captured) {
            incorporateIntoChains(s);
        }

        updateCaptureCount(stone.color, captured.size(), false);
        return move;
    }

    public Move redo() {
        Move move = mHistory.stepForward();
        if (move != null) {
            addStoneWithHistory(move.getStone(), move.getCaptured(), false);
        }
        return move;
    }

    /**
     * Add stone to board, killing opposing groups as necessary
     *
     * @param stone    the stone to add
     * @param captured fills this set with the stones captured
     * @return false if move would be suicide
     */
    public boolean addStone(Stone stone, Set<Stone> captured) {
        return addStoneWithHistory(stone, captured, true);
    }

    public boolean addStoneWithHistory(Stone stone, Set<Stone> captured, boolean modifyHistory) {
        capture(stone, captured);
        if (!isLikeSuicide(stone)) {
            incorporateIntoChains(stone);
            if (modifyHistory) {
                mHistory.add(new Move(stone, captured));
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns up to 4 chains neighboring the supplied coordinates and of the given color
     */
    private Set<Chain> getNeighborChains(Intersection coords, StoneColor color) {
        return new NeighborChecker<Chain>().getMatchingNeighbors(new CheckIntersection<Chain>() {
            public void check(Set<Chain> found, Intersection intersection, Object criterion) {
                if (taken(intersection) && mFilled.get(intersection).getStoneColor() == criterion) {
                    found.add(mFilled.get(intersection));
                }
            }
        }, coords, color);
    }

    /**
     * Checks for and removes opposing chains with this stone as their last liberty
     *
     * @param stone    the stone doing the capturing
     * @param captured fills this set with the stones captured
     */
    private void capture(Stone stone, Set<Stone> captured) {
        Set<Chain> opposingChains = getNeighborChains(stone.intersection, stone.color
                .getOther());
        for (Chain chain : opposingChains) {
            if (chain.isLastLiberty(stone.intersection)) {
                captureChain(chain, captured);
            }
        }
    }

    /**
     * Removes stones in chain from board and increments captureChain count of opposing player
     *
     * @param chain    the chain of stones to captureChain
     * @param captured fills this set with the stones captured
     */
    private void captureChain(Chain chain, Set<Stone> captured) {
        Set<Stone> stones = chain.getStones();
        updateCaptureCount(chain.getStoneColor(), chain.size(), true);
        captured.addAll(stones);
        for (Stone stone : stones) {
            mFilled.remove(stone.intersection);
            addToOpposingLiberties(stone);
        }
        mChains.remove(chain);
    }

    /**
     * Increments/decrements count of opposing colors captures
     */
    private void updateCaptureCount(StoneColor color, int count, boolean increment) {
        if (color == StoneColor.BLACK) {
            mWhitesCaptures += increment ? count : count * -1;
        } else {
            mBlacksCaptures += increment ? count : count * -1;
        }
    }

    public boolean isSuicide(Stone stone) {
        if (!isLikeSuicide(stone)) {
            return false;
        }
        Set<Chain> opposingChains = getNeighborChains(stone.intersection, stone.color
                .getOther());
        for (Chain chain : opposingChains) {
            if (chain.isLastLiberty(stone.intersection)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if stone can be added; false if move would be suicide
     */
    private boolean isLikeSuicide(Stone stone) {
        for (Chain chain : getNeighborChains(stone.intersection, stone.color)) {
            if (!chain.isLastLiberty(stone.intersection)) {
                return false;
            }
        }
        return getLiberties(stone.intersection).size() == 0;
    }

    /**
     * Returns an ArrayList containing coordinates of any liberties of the given position
     */
    private Set<Intersection> getLiberties(Intersection intersection) {
        NeighborChecker<Intersection> neighborChecker = new NeighborChecker<>();

        return new HashSet<>(neighborChecker.getMatchingNeighbors(new CheckIntersection<Intersection>() {
            public void check(Set<Intersection> neighbors, Intersection intersection, Object
                    dummy) {
                if (!taken(intersection)) {
                    neighbors.add(intersection);
                }
            }
        }, intersection, null));
    }

    /**
     * Adds stone to existing chain or creates new chain if not connected to any like-colored chains
     * Merges chains as necessary if new stone connects two or more like-colored chains
     */
    private void incorporateIntoChains(Stone stone) {
        Set<Chain> friends = getNeighborChains(stone.intersection, stone.color);
        Chain merged;
        Iterator<Chain> iterator = friends.iterator();
        if (!iterator.hasNext()) {
            merged = new Chain(stone.color);
            mChains.add(merged);
        } else {
            Chain friend;
            merged = iterator.next();
            while (iterator.hasNext()) {
                friend = iterator.next();
                merged.merge(friend);
                updateFilled(merged, friend.getStones());
                mChains.remove(friend);
            }
        }
        addToChain(merged, stone);
    }

    /**
     * Adds the stone to the chain
     */
    private void addToChain(Chain chain, Stone stone) {
        chain.add(stone, getLiberties(stone.intersection));
        removeFromOpposingLiberties(stone);
        mFilled.put(stone.intersection, chain);
    }

    /**
     * Sets chain as the value for all provided stones
     */
    private void updateFilled(Chain chain, Set<Stone> stones) {
        for (Stone stone : stones) {
            mFilled.put(stone.intersection, chain);
        }
    }

    private void addToOpposingLiberties(Stone stone) {
        for (Chain chain : getNeighborChains(stone.intersection, stone.color.getOther())) {
            chain.addLiberty(stone.intersection);
        }
    }

    private void removeFromOpposingLiberties(Stone stone) {
        for (Chain chain : getNeighborChains(stone.intersection, stone.color.getOther())) {
            chain.removeLiberty(stone.intersection);
        }
    }

    /**
     * Reset to empty board
     */
    public void reset() {
        mChains.clear();
        mHistory.clear();
        mFilled.clear();
        mWhitesCaptures = 0;
        mBlacksCaptures = 0;
    }

    /**
     * Class enabling checking of neighboring coordinates based on some criterion
     */
    private class NeighborChecker<T> {

        /**
         * Returns an Set of the neighbors that matched the criterion
         */
        private Set<T> getMatchingNeighbors(CheckIntersection<T> checker, Intersection coords,
                                            Object
                                                    criterion) {
            Set<T> neighbors = new HashSet<>();
            if (coords.x - 1 > -1) {
                checker.check(neighbors, new Intersection(coords.x - 1, coords.y), criterion);
            }
            if (coords.x + 1 < mBoardSize) {
                checker.check(neighbors, new Intersection(coords.x + 1, coords.y), criterion);
            }
            if (coords.y - 1 > -1) {
                checker.check(neighbors, new Intersection(coords.x, coords.y - 1), criterion);
            }
            if (coords.y + 1 < mBoardSize) {
                checker.check(neighbors, new Intersection(coords.x, coords.y + 1), criterion);
            }
            return neighbors;
        }
    }

    /**
     * Interface to be used as method argument to NeighborChecker class
     */
    private interface CheckIntersection<T> {

        void check(Set<T> found, Intersection intersection, Object criterion);
    }
}

